/**
 * Copyright 2010 Adam Ruggles.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package saig.engine;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import saig.engine.enums.GameStatus;
import saig.engine.models.Model;
import saig.engine.models.Player;
import saig.engine.models.Spawn;
import saig.engine.models.Tile;
import saig.engine.models.enums.ModelType;
import saig.engine.models.enums.TileType;
import saig.engine.models.interfaces.Damageable;


/**
 * Game Engine, this class executes the logic of the game.
 * @author Adam
 * @version $Id$
 * 
 * Created on Jun 3, 2010 at 8:33:47 PM 
 */
public class Engine implements Context {
	/**
	 * The current x position of the view (left point in the view).
	 */
	private float cameraX;
	/**
	 * The current y position of the view (top point in the view).
	 */
	private float cameraY;
	/**
	 * If the game is over or not.
	 */
	private boolean gameOver;
	/**
	 * An array to store the input key states (See key constants).
	 */
	private boolean[] keyState;
	/**
	 * The current working level.
	 */
	private Level level;
	/**
	 * The level length.
	 */
	private int levelLength;
	/**
	 * Logger.
	 */
	private final Logger logger = LoggerFactory.getLogger(Engine.class);
	/**
	 * The player object.
	 */
	private Player player;
	/**
	 * A copy of the level in the original state for resets.
	 */
	private Level resetLevel;
	/**
	 * The current game status.
	 */
	private GameStatus status;
	/**
	 * The amount of time left before the level is complete.
	 */
	private int timeLeft;
	/**
	 * A list of active models for processing updates.
	 */
	private final List<Model> activeModels = new ArrayList<Model>();
	private List<Rectangle> rects = new ArrayList<Rectangle>();
	/**
	 * Constructs the game engine.
	 * @param level The current level for the game.
	 * @param clockTime The amount of time to put on the clock. 
	 */
	public Engine(final Level level, final int clockTime) {
		status = GameStatus.RUNNING;
		setLevel(level);
		timeLeft = clockTime * (int) (Settings.CLOCK_TICKS_PER_SECOND * Settings.FPS);
		cameraX = 0;
		cameraY = 0;
		gameOver = false;
		player = new Player(this);
		player.setX(20);
		for (int i = level.getHeight() - 1; i >= 0; i--) {
			final Tile tile = level.getTile((int)(player.getX() / level.getTileWidth()), i);
			if (tile == null || tile.getType() == TileType.PASSABLE) {
				player.setY((i + 1) * level.getTileHeight() - 1);
				break;
			}
		}
	}
	/**
	 * Renders the scene.
	 * @param g The graphics object to render to.
	 * @param viewSize The dimension of the view.
	 */
	public void doRender(final Graphics g, final int width, final int height) {
		// TODO change 160 from being a magic #.
		final int halfWidth = Settings.SCREEN_WIDTH / 2;
		if (player.getX() > halfWidth && player.getX() <= (levelLength - halfWidth)) {
			cameraX = player.getX() - halfWidth;
		} else if (player.getX() <= halfWidth) {
			cameraX = 0;
		} else if (player.getX() >= (levelLength - halfWidth)) {
			cameraX = levelLength - Settings.SCREEN_WIDTH;
		}
		//logger.debug("Camera [{}, {}] Player [{}, {}]", new Object[] {cameraX, cameraY, player.getX(), player.getY()});
		level.doRender(g, cameraX, cameraY, width, height);
		Color c = g.getColor();
		g.setColor(Color.WHITE);
		// TODO More Magic numbers
		g.drawString(String.valueOf(timeLeft / (int) (Settings.FPS * Settings.CLOCK_TICKS_PER_SECOND)),
				Settings.SCREEN_WIDTH - 40, 20);
		for (Model activeModel : activeModels) {
			activeModel.doRender(g, cameraX, cameraY);
		}
		player.doRender(g, cameraX, cameraY);
		g.setColor(Color.CYAN);
		for (Rectangle in : rects) {
			g.drawRect((int) (in.getX() - cameraX), (int) (in.getY() - cameraY), (int) in.getWidth(), (int) in.getHeight());
		}
		//
		// g.setColor(Color.RED);
		//g.drawRect((int) (player.getX() - (player.getWidth() / 2) - cameraX),
		//		(int) (player.getY() - player.getHeight()),
		//		player.getWidth(), player.getHeight());
		g.setColor(c);
	}
	/**
	 * {@inheritDoc}
	 * @see saig.engine.Context#getEnvironment()
	 */
	public Environment getEnvironment() {
		// TODO create a method that allows returning the current environment.
		return null;
	}
	/**
	 * {@inheritDoc}
	 * @see saig.engine.Context#getKeyState()
	 */
	public boolean[] getKeyState() {
		return keyState;
	}
	/**
	 * {@inheritDoc}
	 * @see saig.engine.Context#getLevel()
	 */
	public Level getLevel() {
		return level;
	}
	/**
	 * Returns status.
	 * @return the status.
	 */
	public GameStatus getStatus() {
		return status;
	}
	/**
	 * Returns timeLeft.
	 * @return the timeLeft.
	 */
	public int getTimeLeft() {
		return timeLeft;
	}
	/**
	 * Returns gameOver.
	 * @return the gameOver.
	 */
	public boolean isGameOver() {
		return gameOver;
	}
	/**
	 * Requests the engine to perform an action based on the given key states.
	 * @param keyState The new key state.
	 */
	public void performAction(final boolean[] keyState) {
		// This can be an unsafe operation.
		this.keyState = keyState;
	}
	/**
	 * {@inheritDoc}
	 * @see saig.engine.Context#reset()
	 */
	public void reset() {
		try {
			level = (Level) resetLevel.clone();
		} catch (final CloneNotSupportedException e) {
			logger.error("Unable to clone the level.", e);
		}
	}
	/**
	 * Sets level.
	 * @param level the level to set.
	 */
	public void setLevel(final Level level) {
		this.resetLevel = level;
		try {
			this.level = (Level) resetLevel.clone();
		} catch (final CloneNotSupportedException e) {
			logger.error("Unable to clone the level.", e);
		}
		levelLength = level.getPixelWidth();
	}
	/**
	 * Sets timeLeft.
	 * @param timeLeft the timeLeft to set.
	 */
	public void setTimeLeft(int timeLeft) {
		this.timeLeft = timeLeft;
	}
	/**
	 * Executes one time slice or tick of the game.
	 */
	public void tick() {
		timeLeft--;
		if (timeLeft < 0 || player.getY() > level.getPixelHeight()) {
			gameOver = true;
			return;
		}
		player.performUpdate();
		if (player.getX() < 0) {
			player.setX(0); player.setDx(0);
		} else if (player.getX() >= levelLength) {
			player.setX(levelLength - 1); player.setDx(0);
		}
		rects.clear();
		List<Model> deadModels = new ArrayList<Model>();
		// Loop through each model and update the model.
		for (Model activeModel : activeModels) {
			activeModel.performUpdate();
			// Collision detection.
			if (activeModel.getBoundingBox().intersects(player.getBoundingBox())) {
				if (activeModel instanceof Damageable) {
					final float deltaY = player.getY() - activeModel.getY();
					if (player.getDy() > 0 && deltaY <= 0 && (!player.isOnGround())) {
						Damageable dm = ((Damageable) activeModel);
						dm.takeDamage();
						player.stomped();
						if (dm.isDead()) {
							deadModels.add(activeModel);
						}
					} else {
						player.takeDamage();
					}
				} else {
					player.takeDamage();
				}
			}
			/*
			if (amRect.intersects(plRect)) {
				Rectangle intersection = amRect.intersection(plRect);
				logger.debug("Hit: " + intersection);
				logger.debug("Player [" + player.getX() + "," + player.getY() + "] Rect = " + player.getBoundingBox());
				logger.debug("Model [" + activeModel.getX() + "," + activeModel.getY() + "] Rect = " + activeModel.getBoundingBox());
				rects.add(intersection);
			}
			*/
		}
		for (Model deadModel : deadModels) {
			activeModels.remove(deadModel);
		}
		// TODO handle collisions.
		
		// TODO handle adding spawns.
		final int startRegion = (int) player.getX() - level.getRegionSize();
		final int endRegion = (int) player.getX() + level.getRegionSize();
		for (int i = startRegion; i < endRegion; i+= level.getRegionSize()) {
			for (Spawn spawn : level.getSpawns(i)) {
				int startSpawnX = (int) player.getX() - (Settings.SCREEN_WIDTH / 2) - (level.getTileWidth() * 2);
				int endSpawnX = (int) player.getX() + (Settings.SCREEN_WIDTH / 2) + (level.getTileWidth() * 2);
				if (player.getX() < Settings.SCREEN_WIDTH / 2) {
					startSpawnX = 0;
					endSpawnX = (int) player.getX() + Settings.SCREEN_WIDTH + (level.getTileWidth() * 2);
				}
				if (!spawn.isActive() && !spawn.isDead()) {
					// TODO perform check along x axis too.
					if (spawn.getX() > startSpawnX && spawn.getX() < endSpawnX) {
						final Model model = spawn.getModel(this);
						if (model.getX() > player.getX()) {
							model.setDirection(-1);
						}
						activeModels.add(model);
						spawn.setActive(true);
					}
				}
			}
		}
		// TODO handle removing spawns.
	}
	/**
	 * {@inheritDoc}
	 * @see saig.engine.Context#tileCollisionOccured(saig.engine.models.enums.ModelType, java.awt.Point)
	 */
	@Override
	public void tileCollisionOccured(ModelType modelType, Point tileLoc) {
		// TODO Auto-generated method stub
		
	}
}
